/**
 * Copyright 2008 Frank van Lingen Licensed under the Apache License, Version
 * 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package superpodder.controller;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingConstants;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import superpodder.model.Feed;
import superpodder.model.Settings;

public class FeedManager extends DefaultHandler {

    private String                           currentGroup   = "";
    /**
     * Track the current name of either the group or the feed itself.
     */
    private String                           currentName    = "";
    /**
     * Track the current xml tag. A saxparser is used so it is necessary to know
     * what action to invoke.
     */
    private String                           currentTag     = "Feeds";
    /**
     * Queue containing the feed jobs for download threads.
     */
    private ConcurrentLinkedQueue<Feed>      queue          = null;
    /**
     * Panel for adding the download info from the different feeds handled by
     * the threads.
     */
    private JPanel                           jPanel         = null;
    /**
     * A parameter to keep track of what the first entry in the history file is,
     * so we can the write the current date (once) to this file, to make it easy
     * for the user to delete certain segments.
     */
    private boolean                          firstEntry     = true;
    /**
     * Total number of feeds at the start of the downloads.
     */
    private int                              totalFeeds     = 0;
    /**
     * Number of feeds that have been successfully downloaded
     */
    private int                              feedsDone      = 0;
    /**
     * total size in MB downloaded.
     */
    private double                           downloadedSize = 0;
    /**
     * Reference to the label that contains the overal count of how many feeds
     * have been downloaded.
     */
    private JLabel                           status         = null;
    /**
     * A sentinel object to help with the layout of the rows top down rather
     * than bottom up.
     */
    private JLabel                           sentinel       = null;
    /**
     * Before downloading the history is read into a hashtable. The assumption
     * is that the history is usually small enought to read into memory (e.g.
     * several MBs). FeedDownloaders check this hashtable before they start
     * downloads.
     */
    protected Hashtable<String, String>      history        = null;
    /**
     * The history file also keeps track of errors to alert a user that certain
     * locations keep failing or have been downloaded before. 
     * This information is stored in an error history
     * which aggregates errors until the last succesful download
     */
    protected Hashtable<String, FeedHistory> errorHistory   = null;

    /**
     * Number of active threads. Threads are active until the queue is empty.
     * This parameter is used to determine when all downloads have finished.
     */
    private int                              activeThreads  = 0;
    /**
     * Contains all threads that where created. Used for accessing the threads
     * to stop downloads.
     */
    private FeedDownloader[]                 feedDownloader = null;
    private long                             startTime      = 0;

    public FeedManager() {
        this.queue = new ConcurrentLinkedQueue<Feed>();

        System.out.println("Instantiating FeedParser");
    }

    /**
     * Threadsafe method to create directories to prevent clashes between
     * threads.
     * 
     * @param path
     *            .The actual (local) directory that needs to be created.
     * @return boolean . True if creation is a success.
     */
    protected synchronized boolean createDirectory(String path) {
        System.out.println("Creating Directory: " + path);
        try {
            // Create one directory
            final boolean success = (new File(path)).mkdirs();
            if (success) {
                System.out.println("Directory: " + path + " created");
            } else {
                System.out.println("Directory: " + path + " already exists");
                return true;
            }
            return true;
        } catch (final Exception e) {// Catch exception if any
            System.err.println("Error: " + e.getMessage());
            return false;
        }
    }

    /**
     * Threadsafe method to return a message with the number of download failures for a rss
     * feed. The outcome depends on the failure type
     */
    protected synchronized String downloadFailure(String url, String failureType) {
        // default message in case we do not find any error history for this failureType
        String msg = "";
        
        if (this.errorHistory.containsKey(url)) {
            
            
            FeedHistory fh = this.errorHistory.get(url);
            System.out.println("test3 "+failureType+" "+fh.type);
           
            if( fh.type.compareTo(failureType) == 0){
                //FIXME: should be case statement
                if( fh.type.compareTo("error") ==0){
                    msg = (fh.count+1) + " errors since "+fh.date+" after last succesful download";
                }
                if( fh.type.compareTo("no_podcast") == 0){
                    msg = (fh.count+1) + " 'no podcasts' alerts, since "+fh.date+" after last succesful download";
                }
                if( fh.type.compareTo("already_downloaded") ==0){
                    msg = (fh.count+1) + " 'already downloaded' alerts, since "+fh.date+" after last succesful download";
                 }   
             
            }
        }
        return msg;
    }

    /**
     * Threadsafe method to display info and progress bar per thread into the
     * main window. These items are created on demand (when a particular feed is
     * downloaded, and not at startup time.
     * 
     * @param feedDownloader
     *            . The thread that is downloading a particular feed.
     */
    protected synchronized void createDownloadStatus(
            FeedDownloader feedDownloader) {
        // remove the sentinel.
        this.jPanel.remove(this.sentinel);

        final GridBagConstraints gbc = new GridBagConstraints();

        // set external padding of components.
        gbc.insets = new Insets(2, 2, 2, 2);

        final GridBagLayout gbl = (GridBagLayout) this.jPanel.getLayout();
        gbc.weightx = 1;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        final JLabel label = new JLabel("Starting a download");
        gbl.setConstraints(label, gbc);
        this.jPanel.add(label);
        // assign the label to the feedDownloader
        // so it can write its progress reports to it.
        feedDownloader.currentLabel = label;

        gbc.weightx = 1;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        final JProgressBar progressBar = new JProgressBar(0, 100);
        progressBar.setValue(0);
        progressBar.setMinimumSize(new Dimension(10, 10));
        progressBar.setStringPainted(true);
        gbl.setConstraints(progressBar, gbc);
        this.jPanel.add(progressBar);
        feedDownloader.currentProgress = progressBar;

        // add the sentinel at the end.
        // FIXME: this sentinel is kind of a hack. There is
        // FIXME: probably a better way of doing this.
        gbc.weightx = 1;
        gbc.weighty = 1;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.BOTH;
        gbl.setConstraints(this.sentinel, gbc);
        this.jPanel.add(this.sentinel);
        this.jPanel.validate();
        // this.jPanel.paintAll(this.jPanel.getGraphics());

    }

    /**
     * helper method to properly populate errorHistory
     */
    private void updateErrorHistory(String timeStamp, String errorType,
            String url) {
        FeedHistory fh = null;
        if (this.errorHistory.containsKey(url)) {
            fh = this.errorHistory.get(url);
            // check if the error needs to be set. If we had a succesful download 
            // the same day then we should not set a failure for it.
            if( fh.type.compareTo("download_success") == 0 && fh.dateLastSeen.compareTo(timeStamp) == 0){
                return;
            }
            
            // if not the same error type remove it as we start counting
            // from the begining.
            if (fh.type != errorType) {
                this.errorHistory.remove(url);
            }
        }
        // add to history (if not already)
        // and add 1 (for number of errors)
        if (!this.errorHistory.containsKey(url)) {
            fh = new FeedHistory(timeStamp, errorType, 0);
            this.errorHistory.put(url, fh);
        }
        // FIXME: clunky way of doing things.
        fh = this.errorHistory.get(url);
        this.errorHistory.remove(url);

        // we do not count failures on the same day as
        // failures as sometimes we do multiple episode download
        // per podcast if 1 succeeds and 5 failed it means
        // it is still working.
        if( fh.dateLastSeen.compareTo(timeStamp) != 0){
            fh.count += 1;
            fh.dateLastSeen = timeStamp;
        }
        
        this.errorHistory.put(url, fh);
    }

    /**
     * loads the history repository and checks for past failures, no feeds or
     * already downloaded podcasts.
     * 
     * @return
     */
    private String loadRepository() {
        final File repo = new File(Settings.history);

        try {
            if (!repo.exists()) {
                repo.createNewFile();
            }

            System.out.println("Reading history file");
            // initialize hashtables
            this.history = new Hashtable<String, String>();

            this.errorHistory = new Hashtable<String, FeedHistory>();

            final BufferedReader input = new BufferedReader(
                    new FileReader(repo));
            String line = null; // not declared within while loop
            String timestamp = "begin";
            while ((line = input.readLine()) != null) {
                System.out.println("History: " + line);
                // check if this is an error, nofeeds, timestamp, already
                // downloaded entry, if so
                // aggregate results. we assume a simple format here where
                // entries do not overlap.
                Boolean skip = false;
                // FIXME: clunky way of doing this comparison.
                if (line.endsWith("error")) {
                    // filter out the bad url
                    String url = line.split(" ")[0];
                    this.updateErrorHistory(timestamp, "error", url);
                    skip = true;
                }
                if (line.endsWith("no_podcast")) {
                    String url = line.split(" ")[0];
                    this.updateErrorHistory(timestamp, "no_podcast", url);
                    skip = true;
                }
                if (line.endsWith("already_downloaded")) {
                    String url = line.split(" ")[0];
                    this.updateErrorHistory(timestamp, "already_downloaded", url);
                    skip = true;
                }
                if (line.startsWith("timestamp")) {
                    timestamp = line.split(" ")[1];
                }
                if (skip != true) {
                    // filter out good url to see if we need to reset
                    // a bad url.
                    String url = line.split(" ")[0];
                    // we do not remove it from the error list we just
                    // mark it as good
                    this.updateErrorHistory(timestamp, "download_success", url);
                    // put it in history as we did a succesful download.
                    this.history.put(line, "read");
                }
            }
            input.close();
            return "0";

        } catch (final Exception se) {
            return se.toString();
        }

    }

    public String downloadFeeds(JPanel jPanel, Boolean download) {
        this.startTime = System.currentTimeMillis();
        this.feedDownloader = new FeedDownloader[Settings.threads];
        this.downloadedSize = 0;
        this.feedsDone = 0;
        this.jPanel = jPanel;
        // load repo for cross reference.
        String outcome = this.loadRepository();
        if (outcome != "0") {
            return outcome;
        }

        // parse the feed xml document into memory
        final String result = this.parseDocument();
        // in case something went wrong, the user
        // needs to be informed through the gui.
        if (result.compareTo("0") != 0) {
            return result;
        }
        // create threads
        FeedDownloader feedDownloader;
        System.out.println("Creating download threads");
        // do not create more threads than there are feeds.
        if (this.queue.size() < Settings.threads) {
            Settings.threads = this.queue.size();
        }
        this.totalFeeds = this.queue.size();
        // Create a global progress label showing the amount of feeds
        // being downloaded.
        final GridBagConstraints gbc = new GridBagConstraints();
        final GridBagLayout gbl = (GridBagLayout) this.jPanel.getLayout();
        gbc.ipadx = 2;
        gbc.ipady = 2;
        gbc.weightx = 1;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.BOTH;
        this.status = new JLabel("0/" + this.queue.size() + " feeds done",
                SwingConstants.CENTER);
        gbl.setConstraints(this.status, gbc);
        this.jPanel.add(this.status);
        this.status.setForeground(Color.black);
        this.status.setBackground(Color.orange);

        gbc.weightx = 1;
        gbc.weighty = 1;
        gbc.gridwidth = GridBagConstraints.REMAINDER;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.ipadx = 2;
        gbc.ipady = 2;
        final JLabel flabel = new JLabel("");
        gbl.setConstraints(flabel, gbc);
        this.jPanel.add(flabel);
        this.sentinel = flabel;

        // start the threads. A simplified threadpool idea
        // is used where the threads read from a job queue
        // that is hosted by the feed manager.
        this.activeThreads = Settings.threads;

        for (int i = 0; i < Settings.threads; i++) {
            feedDownloader = new FeedDownloader(this, download);
            this.feedDownloader[i] = feedDownloader;
            feedDownloader.start();
        }
        return "0";
    }

    public synchronized Feed getNextFeed(boolean reset) {
        if (reset) {
            this.queue = new ConcurrentLinkedQueue<Feed>();
            return null;
        }
        return this.queue.poll();
    }

    protected String parseDocument() {
        // get a factory
        final SAXParserFactory spf = SAXParserFactory.newInstance();
        try {
            // get a new instance of parser
            final SAXParser sp = spf.newSAXParser();
            // parse the file and also register this class for call backs
            System.out.println("Parsing: " + Settings.feeds);
            sp.parse("file:/" + Settings.feeds, this);
        } catch (final Exception se) {
            return "near <" + this.currentTag + " name=\"" + this.currentName
                    + "\".... :\n\n" + se.getMessage();
        }
        return "0";
    }

    /**
     * Trick to easily round numbers to 2 decimals.
     * 
     * @param number 
     * @return
     */
    protected double round2Decimals(double number) {
        return (double) Math.round(number * 100) / (double) 100;
    }

    /**
     * Event handlers for the different xml elements found in the feed file. The
     * format for this file is explained in the help-->content menu in the main
     * window.
     */
    @Override
    public void startElement(String uri, String localName, String qName,
            Attributes attributes) throws SAXException {

        if ((qName.compareTo("Feed") == 0) || (qName.compareTo("Group") == 0)) {
            this.currentName = attributes.getValue("name");
            this.currentTag = qName;
            if (qName.compareTo("Group") == 0) {
                this.currentGroup = this.currentName;
            }
            if (qName.compareTo("Feed") == 0) {
                final Feed feed = new Feed(this.currentGroup, this.currentName,
                        attributes.getValue("url"));
                try {
                    feed.number = new Integer(attributes.getValue("number"));
                } catch (final Exception se) {
                }
                this.queue.add(feed);
            }
        }
    }

    public synchronized void stop() {
        this.getNextFeed(true);
        for (int i = 0; i < this.feedDownloader.length; i++) {
            this.feedDownloader[i].stopDownload = true;
        }
    }

    /**
     * Registers the number of threads that are done, to enable defining a stop
     * criterium.
     */
    protected synchronized void threadDone() {
        this.activeThreads = this.activeThreads - 1;
        System.out.println(this.activeThreads + " active threads");
        if (this.activeThreads == 0) {
            Settings.currentStatus = Settings.status.begin;
        }
    }

    /**
     * Updates the (top) label in the gui with information on feeds handled and amount
     * downloaded. It aggregates all information of all threads and feeds handled.
     */
    protected synchronized void update() {
        this.feedsDone++;
        double timeProgress = (double) (System.currentTimeMillis() - this.startTime)
                / (double) 1000;
        timeProgress = this.round2Decimals(timeProgress);
        double MBs = this.downloadedSize / timeProgress;
        MBs = this.round2Decimals(MBs);
        this.status.setText(this.feedsDone + "/" + this.totalFeeds
                + " feeds done, total size: "
                + this.round2Decimals(this.downloadedSize)
                + " MB, overal speed: " + MBs + " MB/s, elapsed time: "
                + timeProgress + " seconds");
    }

    /**
     * Writes information to the history file. This information is used
     * to display statistics on errors, failures, etc... to the user, so
     * the user can decide to remove certain feeds based on this.
     * 
     * @param file feed url with (optional) error message
     * @param size amount download for this feed
     * @param download determines if it is written to the file or not. If we only
     * run a size estimate of the feeds, we do not write anything to the history file.
     */
    protected synchronized void writeToHistory(String file, double size,
            boolean download) {
        this.downloadedSize += size;
        if (download) {
            try {

                final BufferedWriter out = new BufferedWriter(new FileWriter(
                        Settings.history, true));

                if (this.firstEntry) {
                    this.firstEntry = false;
                    final DateFormat dateFormat = new SimpleDateFormat(
                            "yyyy/MM/dd HH:mm:ss");
                    out.write("timestamp: " + dateFormat.format(new Date())
                            + "\n");
                }
                out.write(file + "\n");
                out.close();
            } catch (final Exception se) {
                System.out.println("Problem writing file: " + se.getMessage());
            }
        }
    }

}
